/**
 * Copyright (C) 2015 Paul Cech
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.eazegraph.lib.charts;

import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.ScrollHelper;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.utils.Color;
import ohos.agp.utils.Point;
import ohos.agp.utils.Rect;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.multimodalinput.event.TouchEvent;
import org.eazegraph.lib.ValueAnimator;
import org.eazegraph.lib.communication.IOnBarClickedListener;
import org.eazegraph.lib.gesture.GestureDetector;
import org.eazegraph.lib.models.BaseModel;
import org.eazegraph.lib.utils.AttrUtils;
import org.eazegraph.lib.utils.LogUtil;
import org.eazegraph.lib.utils.Utils;

import java.util.List;

/**
 * The abstract class for every type of bar chart, which handles the general calculation for the bars.
 */
public abstract class BaseBarChart extends BaseChart {
    /**
     * * The constant DEF_SHOW_VALUES
     */
    public static final boolean DEF_SHOW_VALUES = true;

    /**
     * * The constant DEF_BAR_WIDTH
     */
    public static final float DEF_BAR_WIDTH = 32.f;

    /**
     * * The constant DEF_FIXED_BAR_WIDTH
     */
    public static final boolean DEF_FIXED_BAR_WIDTH = false;

    /**
     * * The constant DEF_BAR_MARGIN
     */
    public static final float DEF_BAR_MARGIN = 12.f;

    /**
     * * The constant DEF_SCROLL_ENABLED
     */
    public static final boolean DEF_SCROLL_ENABLED = true;

    /**
     * * The constant DEF_VISIBLE_BARS
     */
    public static final int DEF_VISIBLE_BARS = 6;

    /**
     * Constructor that is called when inflating a view from XML. This is called
     * when a view is being constructed from an XML file, supplying attributes
     * that were specified in the XML file. This version uses a default style of
     * 0, so the only attribute values applied are those in the Context's Theme
     * and the given AttributeSet.
     * <p/>
     * <p/>
     * The method onFinishInflate() will be called after all children have been
     * added.
     *
     * @param context The Context the view is running in, through which it can                access the current theme, resources, etc.
     * @nt)
     */
    private final String BaseBarChart_egShowValues = "egShowValues";
    /**
     * * The constant BaseBarChart_egBarWidth
     */
    private final String BaseBarChart_egBarWidth = "egBarWidth";
    /**
     * * The constant BaseBarChart_egBarMargin
     */
    private final String BaseBarChart_egBarMargin = "egBarMargin";
    /**
     * * The constant BaseBarChart_egFixedBarWidth
     */
    private final String BaseBarChart_egFixedBarWidth = "egFixedBarWidth";
    /**
     * * The constant BaseBarChart_egEnableScroll
     */
    private final String BaseBarChart_egEnableScroll = "egEnableScroll";
    /**
     * * The constant BaseBarChart_egVisibleBars
     */
    private final String BaseBarChart_egVisibleBars = "egVisibleBars";
    /**
     * * The constant TAG
     */
    private static final String TAG = "BaseBarChart";
    /**
     * * The constant LOG_TAG
     */
    private static final String LOG_TAG = BaseBarChart.class.getSimpleName();
    /**
     * The current viewport. This rectangle represents the currently visible chart domain
     * and range. The currently visible chart X values are from this rectangle's left to its right.
     * The currently visible chart Y values are from this rectangle's top to its bottom.
     * <p>
     * Note that this rectangle's top is actually the smaller Y value, and its bottom is the larger
     * Y value. Since the chart is drawn onscreen in such a way that chart Y values increase
     * towards the top of the screen (decreasing pixel Y positions), this rectangle's "top" is drawn
     * above this rectangle's "bottom" value.
     *
     * @see #mContentRect #mContentRect
     */
    protected RectFloat mCurrentViewport = new RectFloat();
    /**
     * The current destination rectangle (in pixel coordinates) into which the chart data should
     * be drawn. Chart labels are drawn outside this area.
     *
     * @see #mCurrentViewport #mCurrentViewport
     */
    protected Rect mContentRect = new Rect();
    /**
     * The constant M listener
     */
    protected IOnBarClickedListener mListener = null;
    /**
     * The constant M graph paint
     */
    protected Paint mGraphPaint;
    /**
     * The constant M legend paint
     */
    protected Paint mLegendPaint;
    /**
     * The constant M bar width
     */
    protected float mBarWidth;
    /**
     * The constant M fixed bar width
     */
    protected boolean mFixedBarWidth;
    /**
     * The constant M bar margin
     */
    protected float mBarMargin;
    /**
     * The constant M available screen size
     */
    protected int mAvailableScreenSize;

    /**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected boolean mScrollEnabled;
    /**
     * The constant M visible bars
     */
    protected int mVisibleBars;
    /**
     * The constant M show values
     */
    protected boolean mShowValues;
    /**
     * The constant M gesture detector
     */
    private GestureDetector mGestureDetector;
    /**
     * The constant M scroller
     */
    private ScrollHelper mScroller;
    /**
     * The constant M scroll animator
     */
    private ValueAnimator mScrollAnimator;
    /**
     * The gesture listener, used for handling simple gestures such as double touches, scrolls,
     * and flings.
     */
    private final GestureDetector.SimpleOnGestureListener mGestureListener
            = new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onScroll(TouchEvent e1, TouchEvent e2,
                                float distanceX, float distanceY) {
            LogUtil.info(TAG, "onScroll");
            if (mCurrentViewport.left + distanceX > mContentRect.left && mCurrentViewport.right + distanceX < mContentRect.right) {
                mCurrentViewport.left += distanceX;
                mCurrentViewport.right += distanceX;
            }

            if (mCurrentViewport.top + distanceY > mContentRect.top && mCurrentViewport.bottom + distanceY < mContentRect.bottom) {
                mCurrentViewport.top += distanceY;
                mCurrentViewport.bottom += distanceY;
            }

            invalidateGlobal();
            return true;
        }

        @Override
        public boolean onFling(TouchEvent e1, TouchEvent e2, float velocityX, float velocityY) {
            LogUtil.info(TAG, "onFling");
            fling((int) -velocityX, (int) -velocityY);
            return true;
        }

        @Override
        public boolean onDown(TouchEvent event) {
            // The user is interacting with the pie, so we want to turn on acceleration
            // so that the interaction is smooth.
            if (!mScroller.isFinished()) {
                stopScrolling();
            }
            return true;
        }
    };

    /**
     * Simple constructor to use when creating a view from code.
     *
     * @param context The Context the view is running in, through which it can                access the current theme, resources, etc.
     */
    public BaseBarChart(Context context) {
        super(context);

        mShowValues = DEF_SHOW_VALUES;
        mBarWidth = Utils.dpToPx(getContext(),DEF_BAR_WIDTH);
        mBarMargin = Utils.dpToPx(getContext(),DEF_BAR_MARGIN);
        mFixedBarWidth = DEF_FIXED_BAR_WIDTH;
        mScrollEnabled = DEF_SCROLL_ENABLED;
        mVisibleBars = DEF_VISIBLE_BARS;
    }

    /**
     * Base bar chart
     *
     * @param context context
     * @param attrs   attrs
     */
    public BaseBarChart(Context context, AttrSet attrs) {
        super(context, attrs);
        mShowValues = AttrUtils.getBooleanFromAttr(attrs, BaseBarChart_egShowValues, DEF_SHOW_VALUES);
        mBarWidth = AttrUtils.getDimensionFromAttr(attrs, BaseBarChart_egBarWidth, (int) Utils.dpToPx(getContext(),DEF_BAR_WIDTH));
        mBarMargin = AttrUtils.getDimensionFromAttr(attrs, BaseBarChart_egBarMargin, (int) Utils.dpToPx(getContext(),DEF_BAR_MARGIN));
        mFixedBarWidth = AttrUtils.getBooleanFromAttr(attrs, BaseBarChart_egFixedBarWidth, DEF_FIXED_BAR_WIDTH);
        mScrollEnabled = AttrUtils.getBooleanFromAttr(attrs, BaseBarChart_egEnableScroll, DEF_SCROLL_ENABLED);
        mVisibleBars = AttrUtils.getIntFromAttr(attrs, BaseBarChart_egVisibleBars, DEF_VISIBLE_BARS);
    }

    /**
     * Returns the onBarClickedListener.
     *
     * @return on bar clicked listener
     */
    public IOnBarClickedListener getOnBarClickedListener() {
        return mListener;
    }

    /**
     * Sets the onBarClickedListener
     *
     * @param _listener The listener which will be set.
     */
    public void setOnBarClickedListener(IOnBarClickedListener _listener) {
        mListener = _listener;
    }

    /**
     * Returns the width of a bar.
     *
     * @return float
     */
    public float getBarWidth() {
        return mBarWidth;
    }

    /**
     * Sets the width of bars.
     *
     * @param _barWidth Width of bars
     */
    public void setBarWidth(float _barWidth) {
        mBarWidth = _barWidth;
        onDataChanged();
    }

    /**
     * Checks if the bars have a fixed width or is dynamically calculated.
     *
     * @return boolean
     */
    public boolean isFixedBarWidth() {
        return mFixedBarWidth;
    }

    /**
     * Sets if the bar width should be fixed or dynamically caluclated
     *
     * @param _fixedBarWidth True if it should be a fixed width.
     */
    public void setFixedBarWidth(boolean _fixedBarWidth) {
        mFixedBarWidth = _fixedBarWidth;
        onDataChanged();
    }

    /**
     * Returns the bar margin, which is set by user if the bar widths are calculated dynamically.
     *
     * @return float
     */
    public float getBarMargin() {
        return mBarMargin;
    }

    /**
     * Sets the bar margin.
     *
     * @param _barMargin Bar margin
     */
    public void setBarMargin(float _barMargin) {
        mBarMargin = _barMargin;
        onDataChanged();
    }

    /**
     * Is scroll enabled boolean
     *
     * @return the boolean
     */
    public boolean isScrollEnabled() {
        return mScrollEnabled;
    }

    /**
     * Set scroll enabled *
     *
     * @param _scrollEnabled scroll enabled
     */
    public void setScrollEnabled(boolean _scrollEnabled) {
        mScrollEnabled = _scrollEnabled;
        onDataChanged();
    }

    /**
     * Get visible bars int
     *
     * @return the int
     */
    public int getVisibleBars() {
        return mVisibleBars;
    }

    /**
     * Set visible bars *
     *
     * @param _visibleBars visible bars
     */
    public void setVisibleBars(int _visibleBars) {
        mVisibleBars = _visibleBars;
        onDataChanged();
    }

    /**
     * Returns if the values are drawn on top of the bars.
     *
     * @return True if they are drawn.
     */
    public boolean isShowValues() {
        return mShowValues;
    }

    /**
     * Determines if the values of each data should be shown in the graph.
     *
     * @param _showValues true to show values in the graph.
     */
    public void setShowValues(boolean _showValues) {
        mShowValues = _showValues;
        invalidateGlobal();
    }

    /**
     * Set scroll to end
     */
    public void setScrollToEnd() {
        mCurrentViewport.left = mContentRect.getWidth() - mGraphWidth;
        mCurrentViewport.right = mContentRect.getWidth();
        invalidateGlobal();
    }

    /**
     * This is called during layout when the size of this view has changed. If
     * you were just added to the view hierarchy, you're called with the old
     * values of 0.
     *
     * @param component component
     */
    @Override
    public void onRefreshed(Component component) {
        super.onRefreshed(component);
        mAvailableScreenSize = this instanceof VerticalBarChart ? mGraphHeight : mGraphWidth;

        if (getData().size() > 0) {
            onDataChanged();
        }
        setLayoutRefreshedListener(null);        
    }

    /**
     * This is the main entry point after the graph has been inflated. Used to initialize the graph
     * and its corresponding members.
     */
    @Override
    protected void initializeGraph() {
        super.initializeGraph();

        mGraphPaint = new Paint();
        mGraphPaint.setAntiAlias(true);
        mGraphPaint.setStyle(Paint.Style.FILL_STYLE);

        mLegendPaint = new Paint();
        mLegendPaint.setAntiAlias(true);
        mLegendPaint.setColor(new Color(mLegendColor));
        mLegendPaint.setTextSize((int) mLegendTextSize);
        mLegendPaint.setStrokeWidth(2);
        mLegendPaint.setStyle(Paint.Style.FILL_STYLE);

        mMaxFontHeight = Utils.calculateMaxTextHeight(mLegendPaint, null);

        mGestureDetector = new GestureDetector(getContext(), mGestureListener);
        mScroller = new ScrollHelper();

        mRevealAnimator = ValueAnimator.ofFloat(0, 1);
        mRevealAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
            @Override
            public void onUpdate(AnimatorValue animatorValue, float value) {
                mRevealValue = value;
                mGraph.invalidate();
            }
        });
        mRevealAnimator.setStateChangedListener(stateChangedListener);
        // The scroller doesn't have any built-in animation functions--it just supplies
        // values when we ask it to. So we have to have a way to call it every frame
        // until the fling ends. This code (ab)uses a ValueAnimator object to generate
        // a callback on every animation frame. We don't use the animated value at all.
        mScrollAnimator = ValueAnimator.ofFloat();
        mScrollAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
            @Override
            public void onUpdate(AnimatorValue animatorValue, float value) {
                tickScrollAnimation(value);
                invalidateGlobal();
            }
        });
    }

    private Animator.StateChangedListener stateChangedListener = new Animator.StateChangedListener() {
        @Override
        public void onStart(Animator animator) {
        }

        @Override
        public void onStop(Animator animator) {
        }

        @Override
        public void onCancel(Animator animator) {
        }

        @Override
        public void onEnd(Animator animator) {
            mStartedAnimation = false;
        }

        @Override
        public void onPause(Animator animator) {
        }

        @Override
        public void onResume(Animator animator) {
        }
    };

    /**
     * Calculates the bar width and bar margin based on the _DataSize and settings and starts the boundary
     * calculation in child classes.
     *
     * @param _DataSize Amount of data sets
     */
    protected void calculateBarPositions(int _DataSize) {

        int dataSize = mScrollEnabled ? mVisibleBars : _DataSize;
        float barWidth = mBarWidth;
        float margin = mBarMargin;

        if (!mFixedBarWidth) {
            // calculate the bar width if the bars should be dynamically displayed
            barWidth = (mAvailableScreenSize / _DataSize) - margin;
        } else {

            if (_DataSize < mVisibleBars) {
                dataSize = _DataSize;
            }

            // calculate margin between bars if the bars have a fixed width
            float cumulatedBarWidths = barWidth * dataSize;
            float remainingScreenSize = mAvailableScreenSize - cumulatedBarWidths;

            margin = remainingScreenSize / dataSize;
        }

        boolean isVertical = this instanceof VerticalBarChart;

        int calculatedSize = (int) ((barWidth * _DataSize) + (margin * _DataSize));
        int contentWidth = isVertical ? mGraphWidth : calculatedSize;
        int contentHeight = isVertical ? calculatedSize : mGraphHeight;

        mContentRect = new Rect(0, 0, contentWidth, contentHeight);
        mCurrentViewport = new RectFloat(0, 0, mGraphWidth, mGraphHeight);

        calculateBounds(barWidth, margin);
        mLegend.invalidate();
        mGraph.invalidate();
    }

    /**
     * Fling *
     *
     * @param velocityX velocity x
     * @param velocityY velocity y
     */
    private void fling(int velocityX, int velocityY) {
        mScroller.doFling(
                (int) mCurrentViewport.left,
                (int) mCurrentViewport.top,
                velocityX,
                velocityY,
                0, mContentRect.getWidth() - mGraphWidth,
                0, mContentRect.getHeight() - mGraphHeight);

        // Start the animator and tell it to animate for the expected duration of the fling.
        int splineFlingDuration = Utils.getSplineFlingDuration(Math.abs(velocityX), getContext());
        LogUtil.info(TAG, "time" + splineFlingDuration + " velocityX: " + velocityX + "width: "
                + mContentRect.getWidth() + "height: " + mContentRect.getHeight() + " mGraphWidth: " + mGraphWidth
                + " mCurrentViewport.left: " + mCurrentViewport.left);
        mScrollAnimator.setDuration(splineFlingDuration);
        mScrollAnimator.start();
    }

    /**
     * Tick scroll animation *
     *
     * @param value value
     */
    private void tickScrollAnimation(float value) {
        if (!mScroller.isFinished()) {
            LogUtil.info(TAG, "isFinished not");

            mScroller.updateScroll();
            int currX = mScroller.getCurrValue(ScrollHelper.AXIS_X);
            int currY = mScroller.getCurrValue(ScrollHelper.AXIS_Y);
            float currVelocity = mScroller.getCurrVelocity();

            int flingDistanceX = mScroller.getFlingDistanceX(ScrollHelper.AXIS_X);
            int scrollDistanceX = mScroller.getScrollDistanceX();
            LogUtil.info(TAG, "isFinished currX " + currX + " currY: " + currY);
            if (currX > mContentRect.left && currX + mGraphWidth < mContentRect.right) {
                mCurrentViewport.left = currX;
                mCurrentViewport.right = currX + mGraphWidth;
            }

            if (currY > mContentRect.top && currY + mGraphHeight < mContentRect.bottom) {
                mCurrentViewport.top = currY;
                mCurrentViewport.bottom = currY + mGraphHeight;
            }


        } else {

            mScroller.abortAnimation();
        }
    }

    /**
     * Force a stop to all pie motion. Called when the user taps during a fling.
     */
    private void stopScrolling() {

        mScrollAnimator.cancel();
        mScroller.abortAnimation();
    }

    /**
     * Calculates the bar boundaries based on the bar width and bar margin.
     *
     * @param _Width  Calculated bar width
     * @param _Margin Calculated bar margin
     */
    protected abstract void calculateBounds(float _Width, float _Margin);

    /**
     * Callback method for drawing the bars in the child classes.
     *
     * @param _Canvas The canvas object of the graph view.
     */
    protected abstract void drawBars(Canvas _Canvas);

    /**
     * Returns the list of data sets which hold the information about the legend boundaries and text.
     *
     * @return List of BaseModel data sets.
     */
    protected abstract List<? extends BaseModel> getLegendData();

    /**
     * Get bar bounds list
     *
     * @return the list
     */
    protected abstract List<RectFloat> getBarBounds();

    /**
     * On graph draw *
     *
     * @param _Canvas canvas
     */
    @Override
    protected void onGraphDraw(Canvas _Canvas) {
        super.onGraphDraw(_Canvas);
        _Canvas.translate(-mCurrentViewport.left, -mCurrentViewport.top);
        drawBars(_Canvas);
    }

    /**
     * On legend draw *
     *
     * @param _Canvas canvas
     */
    @Override
    protected void onLegendDraw(Canvas _Canvas) {
        super.onLegendDraw(_Canvas);

        _Canvas.translate(-mCurrentViewport.left, 0);

        for (BaseModel model : getLegendData()) {
            if (model.canShowLabel()) {
                RectFloat bounds = model.getLegendBounds();
                _Canvas.drawText(mLegendPaint, model.getLegendLabel(),
                        model.getLegendLabelPosition(), bounds.bottom - mMaxFontHeight);
                _Canvas.drawLine(
                        new Point(bounds.getCenter().getPointX(),
                                bounds.bottom - mMaxFontHeight * 2 - mLegendTopPadding),
                        new Point(bounds.getCenter().getPointX(),
                                mLegendTopPadding), mLegendPaint
                );
            }
        }
    }

    /**
     * On graph overlay touch event boolean
     *
     * @param _Event event
     * @return the boolean
     */
    @Override
    protected boolean onGraphOverlayTouchEvent(TouchEvent _Event) {
        boolean result = mGestureDetector.onTouchEvent(_Event);

        switch (_Event.getAction()) {
            case TouchEvent.PRIMARY_POINT_DOWN:

                result = true;

                if (mListener == null) {
                    // we're not interested in clicks on individual bars here
                    BaseBarChart.this.onTouchEvent(null, _Event);
                } else {
                    float newX = _Event.getPointerPosition(0).getX() + mCurrentViewport.left;
                    float newY = _Event.getPointerPosition(0).getY() + mCurrentViewport.top;
                    int counter = 0;

                    for (RectFloat rectF : getBarBounds()) {
                        if (Utils.intersectsPointWithRectF(rectF, newX, newY)) {
                            mListener.onBarClicked(counter);
                            break; // no need to check other bars
                        }
                        counter++;
                    }
                }
                break;
        }

        return result;
    }

}
